#include "digest.h"

#include <openssl/evp.h>
#include <stdexcept>
#include <sstream>

#include "file.h"


namespace evm {

namespace {

struct AllDigests {
    AllDigests() {
        OpenSSL_add_all_digests();
    }
};

AllDigests all_digests;

}

Digest::Digest(const string& name) {
    const EVP_MD *md = EVP_get_digestbyname(name.c_str());
    if (!md) {  // fix some bug of OpenSSL_add_all_digests
        if (name == "md5")
            md = EVP_md5();
        else if (name == "sha1")
            md = EVP_sha1();
        else if (name == "sha256")
            md = EVP_sha256();
        else if (name == "sha512")
            md = EVP_sha512();
    }
    
    if (!md)
        throw invalid_argument("Unknown message digest " + name);
    mdctx_ = EVP_MD_CTX_create();
    EVP_DigestInit_ex(static_cast<EVP_MD_CTX*>(mdctx_), md, NULL);
}

Digest::~Digest() {
    EVP_MD_CTX_destroy(static_cast<EVP_MD_CTX*>(mdctx_));
}

Digest& Digest::update(const string &arg) {
    if (digested_)
        throw runtime_error("EVP_DigestFinal_ex has been called");
    EVP_DigestUpdate(static_cast<EVP_MD_CTX*>(mdctx_), arg.c_str(), arg.size());
    return *this;
}

const string& Digest::digest() {
    if (!digested_) {
        unsigned char md_value[EVP_MAX_MD_SIZE];
        unsigned md_len;
        EVP_DigestFinal_ex(static_cast<EVP_MD_CTX*>(mdctx_), md_value, &md_len);
        digest_.assign(reinterpret_cast<char*>(md_value), md_len);

        digested_ = true;
    }
    return digest_;
}

const string& Digest::hexdigest() {
    if (!hexdigested_) {
        if (!digested_)
            digest();

        static const char* hex_chars = "0123456789abcdef";
        for (unsigned char c: digest_) {
            hexdigest_ += hex_chars[c / 16];
            hexdigest_ += hex_chars[c % 16];
        }

        hexdigested_ = true;
    }
    return hexdigest_;
}

FileDigest::FileDigest(const string& name, const string& file_name) : Digest(name) {
    static constexpr int CHUNK_SIZE = 4096;

    File f(file_name);
    string bytes;
    bool end = false;
    while (!end) {
        bytes.resize(CHUNK_SIZE);
        size_t count = CHUNK_SIZE;
        end = !f.read(&bytes[0], count);
        bytes.resize(count);
        update(bytes);
    }
}

}
